package org.example.aspect;

import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Pointcut;
import org.aspectj.lang.reflect.MethodSignature;
import org.example.annotation.RepeatSubmit;
import org.example.constant.RedisKey;
import org.example.enums.BizCodeEnum;
import org.example.exception.BizException;
import org.example.interceptor.LoginInterceptor;
import org.example.util.CommonUtil;
import org.redisson.api.RLock;
import org.redisson.api.RedissonClient;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.stereotype.Component;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;

import javax.annotation.Resource;
import javax.servlet.http.HttpServletRequest;
import java.lang.reflect.Method;
import java.util.concurrent.TimeUnit;

/**
 * @description: 定义一个切面类
 * @author: 谢超
 * @date: 2024/7/24 15:40
 */
@Aspect
@Component
@Slf4j
public class RepeatSubmitAspect {

    @Resource
    private StringRedisTemplate stringRedisTemplate;

    @Resource
    private RedissonClient redissonClient;

    /**
     * 定义PointCut注解表达式
     * 方式一: @annotation: 当执行的方法上拥有指定的注解时生效
     * 方式二: execution: 一般用于指定方法的执行 @Pointcut("execution(* com.example.service.*.*(..))")
     */
    @Pointcut("@annotation(repeatSubmit)")
    public void pointCuNoRepeatSumbit(RepeatSubmit repeatSubmit) {

    }

    /**
     * 环绕通知, 围绕着方法执行
     * @Around 可以用来在调用一个具体方法前和调用后来完成一些具体的任务。
     *
     * 方式一：单用@Around("execution(*net.xdclass.controller.*.*(..))")可以
     * 方式二：用@Pointcut和@Around联合注解也可以（我们采用这个）
     *
     * 两种方式
     * 方式一：加锁 固定时间内不能重复提交
     * 方式二：先请求获取token，这边再删除token,删除成功则是第⼀次提交
     *
     * @param joinPoint
     * @param repeatSubmit
     * @return
     * @throws Throwable
     */
    @Around("pointCuNoRepeatSumbit(repeatSubmit)")
    public Object around(ProceedingJoinPoint joinPoint, RepeatSubmit repeatSubmit) throws Throwable {
        HttpServletRequest request = ((ServletRequestAttributes) RequestContextHolder.getRequestAttributes()).getRequest();

        long accountNo = LoginInterceptor.threadLocal.get().getAccountNo();

        // 用于记录结果的成功或者失败
        boolean res = false;

        // 获取参数-防重提交类型
        String type = repeatSubmit.limitType().name();

        if (type.equalsIgnoreCase(RepeatSubmit.Type.PARAM.name())) {
            // 方式一 参数形式防重提交
            long lockTime = repeatSubmit.lockTime();

            String ipAddr = CommonUtil.getIpAddr(request);

            MethodSignature methodSignature = (MethodSignature)joinPoint.getSignature();

            Method method = methodSignature.getMethod();

            String className = method.getDeclaringClass().getName();

            String key = "order_server:repeat_submit:" + CommonUtil.MD5(String.format("%s-%s-%s-%s", ipAddr, className, method, accountNo));

            // 加锁(方式一)
            // res = stringRedisTemplate.opsForValue().setIfAbsent(key, "1", lockTime, TimeUnit.SECONDS);

            RLock lock = redissonClient.getLock(key);

            // 尝试加锁(方式二)，最多等待0秒，上锁以后5秒自动解锁，lockTime默认为5s，可以自定义
            res = lock.tryLock(0, lockTime, TimeUnit.SECONDS );

        } else {
            // 方式二 令牌形式防重提交
            String requestToken = request.getHeader("request-token");
            if (StringUtils.isBlank(requestToken)) {
                throw new BizException(BizCodeEnum.ORDER_CONFIRM_PRICE_FAIL);
            }

            // 通过用户accountNo和用户传来的token值，按照格式生成redis需要的key的格式字符串
            String key = String.format(RedisKey.SUBMIT_ORDER_TOKEN_KEY, accountNo, requestToken);

            log.info("key:{}", key);

            /**
             * 删除redis中的key
             * 方式一：不用lua脚本获取再判断，之前是因为key组成是 order:submit:accountNo, value是对应的token，所以需要先获取值，再判断
             * 方式二：可以直接key是order:submit:accountNo:token,然后直接删除成功则完成
             */
            res = stringRedisTemplate.delete(key);
        }

        if (!res) {
            log.error("订单重复提交");
            return null;
        }

        log.info("环绕通知执行前");
        Object proceed = joinPoint.proceed();
        log.info("环绕通知执行后");

        return proceed;
    }
}
